/** reference from https://github.com/microsoft/vscode */
interface Item<K, V> {
    previous: Item<K, V> | undefined;
    next: Item<K, V> | undefined;
    key: K;
    value: V
}

export const enum Touch {
    None = 0,
    AsOld = 1,
    AsNew = 2
}

export class LinkedMap<K, V> implements Map<K, V> {
    readonly [Symbol.toStringTag] = 'LinkedMap';

    private _map: Map<K, Item<K, V>>;
    private _head: Item<K, V> | undefined;
    private _tail: Item<K, V> | undefined;
    private _size: number;

    private _state: number;

    constructor() {
        this._map = new Map<K, Item<K, V>>();
        this._head = undefined;
        this._tail = undefined;
        this._size = 0;
        this._state = 0;
    }

    clear(): void {
        this._map.clear();
        this._head = undefined;
        this._tail = undefined;
        this._size = 0;
        this._state++;
    }

    isEmpty(): boolean {
        return !this._head && !this._tail;
    }

    get size(): number {
        return this._size;
    }

    get first(): V | undefined {
        return this._head?.value;
    }

    get last(): V | undefined {
        return this._tail?.value;
    }

    has(key: K): boolean {
        return this._map.has(key);
    }

    get(key: K, touch: Touch = Touch.None): V | undefined {
        const item = this._map.get(key);
        if (!item) {
            return undefined;
        }
        if (touch !== Touch.None) {
            this.touch(item, touch);
        }
        return item.value;
    }

    set(key: K, value: V, touch: Touch = Touch.None): this {
        let item = this._map.get(key);
        if (item) {
            item.value = value;
            if (touch !== Touch.None) {
                this.touch(item, touch);
            }
        } else {
            item = { key, value, next: undefined, previous: undefined };
            switch (touch) {
                case Touch.None:
                    this.addItemLast(item);
                    break;
                case Touch.AsOld:
                    this.addItemFirst(item);
                    break;
                case Touch.AsNew:
                    this.addItemLast(item);
                    break;
                default:
                    this.addItemLast(item);
                    break;
            }
            this._map.set(key, item);
            this._size++;
        }
        return this;
    }

    delete(key: K): boolean {
        return !!this.remove(key);
    }

    remove(key: K): V | undefined {
        const item = this._map.get(key);
        if (!item) {
            return undefined;
        }
        this._map.delete(key);
        this.removeItem(item);
        this._size--;
        return item.value;
    }

    shift(): V | undefined {
        if (!this._head && !this._tail) {
            return undefined;
        }
        if (!this._head || !this._tail) {
            throw new Error('Invalid list');
        }
        const item = this._head;
        this._map.delete(item.key);
        this.removeItem(item);
        this._size--;
        return item.value;
    }

    forEach(
        callbackfn: (value: V, key: K, map: LinkedMap<K, V>) => void,
        thisArg?: any
    ): void {
        const state = this._state;
        let current = this._head;
        while (current) {
            if (thisArg) {
                callbackfn.bind(thisArg)(current.value, current.key, this);
            } else {
                callbackfn(current.value, current.key, this);
            }
            if (this._state !== state) {
                throw new Error(`LinkedMap got modified during iteration.`);
            }
            current = current.next;
        }
    }

    keys(): IterableIterator<K> {
        const map = this;
        const state = this._state;
        let current = this._head;
        const iterator: IterableIterator<K> = {
            [Symbol.iterator]() {
                return iterator;
            },
            next(): IteratorResult<K> {
                if (map._state !== state) {
                    throw new Error(`LinkedMap got modified during iteration.`);
                }
                if (current) {
                    const result = { value: current.key, done: false };
                    current = current.next;
                    return result;
                } else {
                    return { value: undefined, done: true };
                }
            }
        };
        return iterator;
    }

    values(): IterableIterator<V> {
        const map = this;
        const state = this._state;
        let current = this._head;
        const iterator: IterableIterator<V> = {
            [Symbol.iterator]() {
                return iterator;
            },
            next(): IteratorResult<V> {
                if (map._state !== state) {
                    throw new Error(`LinkedMap got modified during iteration.`);
                }
                if (current) {
                    const result = { value: current.value, done: false };
                    current = current.next;
                    return result;
                } else {
                    return { value: undefined, done: true };
                }
            }
        };
        return iterator;
    }

    entries(): IterableIterator<[K, V]> {
        const map = this;
        const state = this._state;
        let current = this._head;
        const iterator: IterableIterator<[K, V]> = {
            [Symbol.iterator]() {
                return iterator;
            },
            next(): IteratorResult<[K, V]> {
                if (map._state !== state) {
                    throw new Error(`LinkedMap got modified during iteration.`);
                }
                if (current) {
                    const result: IteratorResult<[K, V]> = {
                        value: [current.key, current.value],
                        done: false
                    };
                    current = current.next;
                    return result;
                } else {
                    return { value: undefined, done: true };
                }
            }
        };
        return iterator;
    }

    [Symbol.iterator](): IterableIterator<[K, V]> {
        return this.entries();
    }

    protected trimOld(newSize: number) {
        if (newSize >= this.size) {
            return;
        }
        if (newSize === 0) {
            this.clear();
            return;
        }
        let current = this._head;
        let currentSize = this.size;
        while (current && currentSize > newSize) {
            this._map.delete(current.key);
            current = current.next;
            currentSize--;
        }
        this._head = current;
        this._size = currentSize;
        if (current) {
            current.previous = undefined;
        }
        this._state++;
    }

    protected trimNew(newSize: number) {
        if (newSize >= this.size) {
            return;
        }
        if (newSize === 0) {
            this.clear();
            return;
        }
        let current = this._tail;
        let currentSize = this.size;
        while (current && currentSize > newSize) {
            this._map.delete(current.key);
            current = current.previous;
            currentSize--;
        }
        this._tail = current;
        this._size = currentSize;
        if (current) {
            current.next = undefined;
        }
        this._state++;
    }

    private addItemFirst(item: Item<K, V>): void {
        // First time Insert
        if (!this._head && !this._tail) {
            this._tail = item;
        } else if (!this._head) {
            throw new Error('Invalid list');
        } else {
            item.next = this._head;
            this._head.previous = item;
        }
        this._head = item;
        this._state++;
    }

    private addItemLast(item: Item<K, V>): void {
        // First time Insert
        if (!this._head && !this._tail) {
            this._head = item;
        } else if (!this._tail) {
            throw new Error('Invalid list');
        } else {
            item.previous = this._tail;
            this._tail.next = item;
        }
        this._tail = item;
        this._state++;
    }

    private removeItem(item: Item<K, V>): void {
        if (item === this._head && item === this._tail) {
            this._head = undefined;
            this._tail = undefined;
        } else if (item === this._head) {
            // This can only happen if size === 1 which is handled
            // by the case above.
            if (!item.next) {
                throw new Error('Invalid list');
            }
            item.next.previous = undefined;
            this._head = item.next;
        } else if (item === this._tail) {
            // This can only happen if size === 1 which is handled
            // by the case above.
            if (!item.previous) {
                throw new Error('Invalid list');
            }
            item.previous.next = undefined;
            this._tail = item.previous;
        } else {
            const next = item.next;
            const previous = item.previous;
            if (!next || !previous) {
                throw new Error('Invalid list');
            }
            next.previous = previous;
            previous.next = next;
        }
        item.next = undefined;
        item.previous = undefined;
        this._state++;
    }

    private touch(item: Item<K, V>, touch: Touch): void {
        if (!this._head || !this._tail) {
            throw new Error('Invalid list');
        }
        if (touch !== Touch.AsOld && touch !== Touch.AsNew) {
            return;
        }

        if (touch === Touch.AsOld) {
            if (item === this._head) {
                return;
            }

            const next = item.next;
            const previous = item.previous;

            // Unlink the item
            if (item === this._tail) {
                // previous must be defined since item was not head but is tail
                // So there are more than on item in the map
                previous!.next = undefined;
                this._tail = previous;
            } else {
                // Both next and previous are not undefined since item was neither head nor tail.
                next!.previous = previous;
                previous!.next = next;
            }

            // Insert the node at head
            item.previous = undefined;
            item.next = this._head;
            this._head.previous = item;
            this._head = item;
            this._state++;
        } else if (touch === Touch.AsNew) {
            if (item === this._tail) {
                return;
            }

            const next = item.next;
            const previous = item.previous;

            // Unlink the item.
            if (item === this._head) {
                // next must be defined since item was not tail but is head
                // So there are more than on item in the map
                next!.previous = undefined;
                this._head = next;
            } else {
                // Both next and previous are not undefined since item was neither head nor tail.
                next!.previous = previous;
                previous!.next = next;
            }
            item.next = undefined;
            item.previous = this._tail;
            this._tail.next = item;
            this._tail = item;
            this._state++;
        }
    }

    toJSON(): [K, V][] {
        const data: [K, V][] = [];

        this.forEach((value, key) => {
            data.push([key, value]);
        });

        return data;
    }

    fromJSON(data: [K, V][]): void {
        this.clear();

        for (const [key, value] of data) {
            this.set(key, value);
        }
    }
}

abstract class Cache<K, V> extends LinkedMap<K, V> {
    protected _limit: number;
    protected _ratio: number;

    constructor(limit: number, ratio: number = 1) {
        super();
        this._limit = limit;
        this._ratio = Math.min(Math.max(0, ratio), 1);
    }

    get limit(): number {
        return this._limit;
    }

    set limit(limit: number) {
        this._limit = limit;
        this.checkTrim();
    }

    get ratio(): number {
        return this._ratio;
    }

    set ratio(ratio: number) {
        this._ratio = Math.min(Math.max(0, ratio), 1);
        this.checkTrim();
    }

    override get(key: K, touch: Touch = Touch.AsNew): V | undefined {
        return super.get(key, touch);
    }

    peek(key: K): V | undefined {
        return super.get(key, Touch.None);
    }

    override set(key: K, value: V): this {
        super.set(key, value, Touch.AsNew);
        return this;
    }

    protected checkTrim() {
        if (this.size > this._limit) {
            this.trim(Math.round(this._limit * this._ratio));
        }
    }

    protected abstract trim(newSize: number): void;
}

export class LRUCache<K, V> extends Cache<K, V> {
    constructor(limit: number, ratio: number = 1) {
        super(limit, ratio);
    }

    protected override trim(newSize: number) {
        this.trimOld(newSize);
    }

    override set(key: K, value: V): this {
        super.set(key, value);
        this.checkTrim();
        return this;
    }
}
